package clientcore

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"sync"
	"time"

	"gitee.com/huanghua_2017/blade/comm"
	"gitee.com/huanghua_2017/blade/types"
	"go.opencensus.io/trace"
	"golang.org/x/xerrors"

	"github.com/gorilla/websocket"
)

func (slf *Core) connServer(ctx context.Context, h types.HelloReqStruct) (*websocket.Conn, *types.HelloResStruct, error) {
	ctx, span := trace.StartSpan(ctx, "client.connServer")
	defer span.End()

	url := "ws://" + slf.Cfg.ServerAddrs[slf.Cfg.Index] + "/ws"
	span.AddAttributes(trace.StringAttribute("proto", h.Proto))
	span.AddAttributes(trace.StringAttribute("address", h.Address))
	span.AddAttributes(trace.StringAttribute("upserver_addr", url))

	conn, _, err := websocket.DefaultDialer.Dial(url, nil)
	if err != nil {
		span.AddAttributes(trace.StringAttribute("result", err.Error()))
		return nil, nil, err
	}
	span.AddAttributes(trace.StringAttribute("result", "ok"))
	msg, _ := json.Marshal(h)
	comm.WriteWSMessage(conn, msg)

	conn.SetReadDeadline(time.Now().Add(time.Second * 5))
	_, msg, err = conn.ReadMessage()
	if err != nil {
		conn.Close()
		span.AddAttributes(trace.StringAttribute("result", err.Error()))
		return nil, nil, err
	}

	msg = comm.DecodeMessage(msg)
	res := types.HelloResStruct{}
	err = json.Unmarshal(msg, &res)
	if err != nil || res.Code != 0 {
		conn.Close()
		span.AddAttributes(trace.StringAttribute("result", fmt.Sprintf("%v, %d : %s", err, res.Code, res.ErrMsg)))
		return nil, nil, xerrors.Errorf("%v, %s", err, res.ErrMsg)
	}
	span.AddAttributes(trace.StringAttribute("result", "ok"))
	return conn, &res, nil
}

func (slf *Core) connectUpstream(ctx context.Context, tcpconn net.Conn, addr string) (chan []byte, error) {
	conn, _, err := slf.connServer(ctx, types.HelloReqStruct{Proto: types.PROXY, Address: addr})
	slf.logger.Printf("连接代理服务器: %s, %s %+v\n", slf.Cfg.ServerAddrs[slf.Cfg.Index], addr, err)
	if err != nil {
		return nil, err
	}
	return slf.proxyExchange(addr, tcpconn, conn), nil
}

func (slf *Core) proxyExchange(addr string, tconn net.Conn, wconn *websocket.Conn) chan []byte {
	wsaddr := wconn.RemoteAddr().String()
	tcpaddr := tconn.RemoteAddr().String()
	var once sync.Once
	closed := make(chan struct{}, 1)
	wschan := make(chan []byte, 1000)
	tcpchan := make(chan []byte, 1000)
	slf.state.UpdateALiveConn(1)
	slf.state.UpdateConnsInfo(addr, 1)
	exitfun := func() {
		close(closed)
		slf.state.UpdateALiveConn(-1)
		slf.state.UpdateConnsInfo(addr, -1)
	}
	go func() {
		for {
			select {
			case <-closed:
				slf.logger.Printf("管道被关闭,关闭客户端连接: %s\n", tcpaddr)
				tconn.Close()
				return
			default:
				data := make([]byte, 4096)
				data[0] = '1'

				tconn.SetReadDeadline(time.Now().Add(time.Second * 1))
				n, err := tconn.Read(data[1:])
				if err != nil && !comm.TimeOutError(err) {
					slf.logger.Printf("客户端断开连接: %s, %+v\n", tcpaddr, err)
					once.Do(exitfun)
					return
				}
				if n > 0 {
					wschan <- data[0 : n+1]
				}
			}
		}
	}()

	go func() {
		for {
			select {
			case <-closed:
				slf.logger.Printf("管道被关闭,关闭上游连接: %s\n", wsaddr)
				wconn.Close()
				return
			default:
				wconn.SetReadDeadline(time.Now().Add(time.Second * 5))
				_, msg, err := wconn.ReadMessage()
				if err != nil {
					slf.logger.Printf("上游服务器断开连接: %s, %+v\n", wsaddr, err)
					once.Do(exitfun)
					return
				}

				l := len(msg)
				if l > 0 {
					msg = comm.DecodeMessage(msg)
					if msg[0] != '0' {
						tcpchan <- msg[1:]
					}
				}
			}
		}
	}()

	go func() {
		tc := time.NewTicker(time.Second * 3).C
		for {
			for {
				select {
				case <-closed:
					slf.logger.Printf("%s -> %s 连接已断开, 退出数据发送协程", tcpaddr, wsaddr)
					return
				case <-tc:
					comm.WriteWSMessage(wconn, []byte{'0'})
				case wmsg := <-wschan:
					err := comm.WriteWSMessage(wconn, wmsg)
					if err != nil {
						slf.logger.Printf("发送数据失败: %s, %+v\n", wconn.RemoteAddr().String(), err)
						once.Do(exitfun)
						return
					}
				case tcpmsg := <-tcpchan:
					err := comm.WriteAll(tconn, tcpmsg)
					if err != nil {
						slf.logger.Printf("发送数据失败: %s, %+v\n", wconn.RemoteAddr().String(), err)
						once.Do(exitfun)
						return
					}
				}
			}
		}
	}()

	return wschan
}
